/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.search;

import java.util.*;

import org.apache.commons.io.FilenameUtils;
import org.apache.lucene.queries.function.FunctionQuery;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.queries.function.valuesource.QueryValueSource;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.search.Query;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.MapSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.transform.DocTransformer;
import org.apache.solr.response.transform.DocTransformers;
import org.apache.solr.response.transform.RenameFieldTransformer;
import org.apache.solr.response.transform.ScoreAugmenter;
import org.apache.solr.response.transform.TransformerFactory;
import org.apache.solr.response.transform.ValueSourceAugmenter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A class representing the return fields
 * 
 * 
 * @since solr 4.0
 */
public class ReturnFields {
	static final Logger log = LoggerFactory.getLogger(ReturnFields.class);

	// Special Field Keys
	public static final String SCORE = "score";

	private final List<String> globs = new ArrayList<String>(1);

	// The lucene field names to request from the SolrIndexSearcher
	// Order is important for CSVResponseWriter
	private final Set<String> fields = new LinkedHashSet<String>();

	// Field names that are OK to include in the response.
	// This will include pseudo fields, lucene fields, and matching globs
	private Set<String> okFieldNames = new HashSet<String>();

	// The list of explicitly requested fields
	private Set<String> reqFieldNames = null;

	private DocTransformer transformer;
	private boolean _wantsScore = false;
	private boolean _wantsAllFields = false;

	public ReturnFields() {
		_wantsAllFields = true;
	}

	public ReturnFields(SolrQueryRequest req) {
		this(req.getParams().getParams(CommonParams.FL), req);
	}

	public ReturnFields(String fl, SolrQueryRequest req) {
		// this( (fl==null)?null:SolrPluginUtils.split(fl), req );
		if (fl == null) {
			parseFieldList((String[]) null, req);
		} else {
			if (fl.trim().length() == 0) {
				// legacy thing to support fl=' ' => fl=*,score!
				// maybe time to drop support for this?
				// See ConvertedLegacyTest
				_wantsScore = true;
				_wantsAllFields = true;
				transformer = new ScoreAugmenter(SCORE);
			} else {
				parseFieldList(new String[] { fl }, req);
			}
		}
	}

	public ReturnFields(String[] fl, SolrQueryRequest req) {
		parseFieldList(fl, req);
	}

	private void parseFieldList(String[] fl, SolrQueryRequest req) {
		_wantsScore = false;
		_wantsAllFields = false;
		if (fl == null || fl.length == 0 || fl.length == 1
				&& fl[0].length() == 0) {
			_wantsAllFields = true;
			return;
		}

		NamedList<String> rename = new NamedList<String>();
		DocTransformers augmenters = new DocTransformers();
		for (String fieldList : fl) {
			add(fieldList, rename, augmenters, req);
		}
		for (int i = 0; i < rename.size(); i++) {
			String from = rename.getName(i);
			String to = rename.getVal(i);
			okFieldNames.add(to);
			boolean copy = (reqFieldNames != null && reqFieldNames
					.contains(from));
			if (!copy) {
				// Check that subsequent copy/rename requests have the field
				// they need to copy
				for (int j = i + 1; j < rename.size(); j++) {
					if (from.equals(rename.getName(j))) {
						rename.setName(j, to); // copy from the current target
						if (reqFieldNames == null) {
							reqFieldNames = new HashSet<String>();
						}
						reqFieldNames.add(to); // don't rename our current
												// target
					}
				}
			}
			augmenters
					.addTransformer(new RenameFieldTransformer(from, to, copy));
		}

		if (!_wantsAllFields) {
			if (!globs.isEmpty()) {
				// TODO??? need to fill up the fields with matching field names
				// in the index
				// and add them to okFieldNames?
				// maybe just get all fields?
				// this would disable field selection optimization... i think
				// thatis OK
				fields.clear(); // this will get all fields, and use wantsField
								// to limit
			}
			okFieldNames.addAll(fields);
		}

		if (augmenters.size() == 1) {
			transformer = augmenters.getTransformer(0);
		} else if (augmenters.size() > 1) {
			transformer = augmenters;
		}
	}

	// like getId, but also accepts dashes for legacy fields
	String getFieldName(QueryParsing.StrParser sp) {
		sp.eatws();
		int id_start = sp.pos;
		char ch;
		if (sp.pos < sp.end && (ch = sp.val.charAt(sp.pos)) != '$'
				&& Character.isJavaIdentifierStart(ch)) {
			sp.pos++;
			while (sp.pos < sp.end) {
				ch = sp.val.charAt(sp.pos);
				if (!Character.isJavaIdentifierPart(ch) && ch != '.'
						&& ch != '-') {
					break;
				}
				sp.pos++;
			}
			return sp.val.substring(id_start, sp.pos);
		}

		return null;
	}

	private void add(String fl, NamedList<String> rename,
			DocTransformers augmenters, SolrQueryRequest req) {
		if (fl == null) {
			return;
		}
		try {
			QueryParsing.StrParser sp = new QueryParsing.StrParser(fl);

			for (;;) {
				sp.opt(',');
				sp.eatws();
				if (sp.pos >= sp.end)
					break;

				int start = sp.pos;

				// short circuit test for a really simple field name
				String key = null;
				String field = getFieldName(sp);
				char ch = sp.ch();

				if (field != null) {
					if (sp.opt(':')) {
						// this was a key, not a field name
						key = field;
						field = null;
						sp.eatws();
						start = sp.pos;
					} else {
						if (Character.isWhitespace(ch) || ch == ',' || ch == 0) {
							addField(field, key, augmenters, req);
							continue;
						}
						// an invalid field name... reset the position pointer
						// to retry
						sp.pos = start;
						field = null;
					}
				}

				if (key != null) {
					// we read "key : "
					field = sp.getId(null);
					ch = sp.ch();
					if (field != null
							&& (Character.isWhitespace(ch) || ch == ',' || ch == 0)) {
						rename.add(field, key);
						addField(field, key, augmenters, req);
						continue;
					}
					// an invalid field name... reset the position pointer to
					// retry
					sp.pos = start;
					field = null;
				}

				if (field == null) {
					// We didn't find a simple name, so let's see if it's a
					// globbed field name.
					// Globbing only works with field names of the recommended
					// form (roughly like java identifiers)

					field = sp.getGlobbedId(null);
					ch = sp.ch();
					if (field != null
							&& (Character.isWhitespace(ch) || ch == ',' || ch == 0)) {
						// "*" looks and acts like a glob, but we give it
						// special treatment
						if ("*".equals(field)) {
							_wantsAllFields = true;
						} else {
							globs.add(field);
						}
						continue;
					}

					// an invalid glob
					sp.pos = start;
				}

				String funcStr = sp.val.substring(start);

				// Is it an augmenter of the form [augmenter_name foo=1
				// bar=myfield]?
				// This is identical to localParams syntax except it uses []
				// instead of {!}

				if (funcStr.startsWith("[")) {
					Map<String, String> augmenterArgs = new HashMap<String, String>();
					int end = QueryParsing.parseLocalParams(funcStr, 0,
							augmenterArgs, req.getParams(), "[", ']');
					sp.pos += end;

					// [foo] is short for [type=foo] in localParams syntax
					String augmenterName = augmenterArgs.remove("type");
					String disp = key;
					if (disp == null) {
						disp = '[' + augmenterName + ']';
					}

					TransformerFactory factory = req.getCore()
							.getTransformerFactory(augmenterName);
					if (factory != null) {
						MapSolrParams augmenterParams = new MapSolrParams(
								augmenterArgs);
						augmenters.addTransformer(factory.create(disp,
								augmenterParams, req));
					} else {
						// unknown transformer?
					}
					addField(field, disp, augmenters, req);
					continue;
				}

				// let's try it as a function instead
				QParser parser = QParser.getParser(funcStr,
						FunctionQParserPlugin.NAME, req);
				Query q = null;
				ValueSource vs = null;

				try {
					if (parser instanceof FunctionQParser) {
						FunctionQParser fparser = (FunctionQParser) parser;
						fparser.setParseMultipleSources(false);
						fparser.setParseToEnd(false);

						q = fparser.getQuery();

						if (fparser.localParams != null) {
							if (fparser.valFollowedParams) {
								// need to find the end of the function query
								// via the string parser
								int leftOver = fparser.sp.end - fparser.sp.pos;
								sp.pos = sp.end - leftOver; // reset our parser
															// to the same
															// amount of
															// leftover
							} else {
								// the value was via the "v" param in
								// localParams, so we need to find
								// the end of the local params themselves to
								// pick up where we left off
								sp.pos = start + fparser.localParamsEnd;
							}
						} else {
							// need to find the end of the function query via
							// the string parser
							int leftOver = fparser.sp.end - fparser.sp.pos;
							sp.pos = sp.end - leftOver; // reset our parser to
														// the same amount of
														// leftover
						}
					} else {
						// A QParser that's not for function queries.
						// It must have been specified via local params.
						q = parser.getQuery();

						assert parser.getLocalParams() != null;
						sp.pos = start + parser.localParamsEnd;
					}

					if (q instanceof FunctionQuery) {
						vs = ((FunctionQuery) q).getValueSource();
					} else {
						vs = new QueryValueSource(q, 0.0f);
					}

					if (key == null) {
						SolrParams localParams = parser.getLocalParams();
						if (localParams != null) {
							key = localParams.get("key");
						}
						if (key == null) {
							// use the function name itself as the field name
							key = sp.val.substring(start, sp.pos);
						}
					}

					if (key == null) {
						key = funcStr;
					}
					okFieldNames.add(key);
					okFieldNames.add(funcStr);
					augmenters.addTransformer(new ValueSourceAugmenter(key,
							parser, vs));
				} catch (ParseException e) {
					// try again, simple rules for a field name with no
					// whitespace
					sp.pos = start;
					field = sp.getSimpleString();

					if (req.getSchema().getFieldOrNull(field) != null) {
						// OK, it was an oddly named field
						fields.add(field);
						if (key != null) {
							rename.add(field, key);
						}
					} else {
						throw new SolrException(
								SolrException.ErrorCode.BAD_REQUEST,
								"Error parsing fieldname: " + e.getMessage(), e);
					}
				}

				// end try as function

			} // end for(;;)
		} catch (ParseException e) {
			throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
					"Error parsing fieldname", e);
		}
	}

	private void addField(String field, String key, DocTransformers augmenters,
			SolrQueryRequest req) {
		if (key == null) {
			if (reqFieldNames == null) {
				reqFieldNames = new HashSet<String>();
			}
			reqFieldNames.add(field);
		}

		fields.add(field); // need to put in the map to maintain order for
							// things like CSVResponseWriter
		okFieldNames.add(field);
		okFieldNames.add(key);
		// a valid field name
		if (SCORE.equals(field)) {
			_wantsScore = true;

			String disp = (key == null) ? field : key;
			augmenters.addTransformer(new ScoreAugmenter(disp));
		}
	}

	public Set<String> getLuceneFieldNames() {
		return (_wantsAllFields || fields.isEmpty()) ? null : fields;
	}

	public boolean wantsAllFields() {
		return _wantsAllFields;
	}

	public boolean wantsScore() {
		return _wantsScore;
	}

	public boolean wantsField(String name) {
		if (_wantsAllFields || okFieldNames.contains(name)) {
			return true;
		}
		for (String s : globs) {
			// TODO something better?
			if (FilenameUtils.wildcardMatch(name, s)) {
				okFieldNames.add(name); // Don't calculate it again
				return true;
			}
		}
		return false;
	}

	public DocTransformer getTransformer() {
		return transformer;
	}
}
